2 * Copyright (c) 2017-2019 Apple Inc. All rights reserved.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 #import "CNServiceBrowserView.h"
18 #import "CNDomainBrowserPathUtils.h"
21 #import <SafariServices/SafariServices.h>
23 #define SHOW_SERVICETYPE_IF_SEARCH_COUNT 0
25 const NSString * _CNInstanceKey_fullName = @"fullName";
26 const NSString * _CNInstanceKey_name = @"name";
27 const NSString * _CNInstanceKey_serviceType = @"serviceType";
28 const NSString * _CNInstanceKey_domainPath = @"domainPath";
29 const NSString * _CNInstanceKey_resolveUrl = @"resolveUrl";
30 const NSString * _CNInstanceKey_resolveInstance = @"resolveInstance";
32 @interface _DNSServiceRefWrapper : NSObject
37 - (instancetype)initWithRef:(DNSServiceRef)ref;
40 @implementation _DNSServiceRefWrapper
42 - (instancetype)initWithRef:(DNSServiceRef)ref
44 if( self = [super init] )
53 if( _ref ) DNSServiceRefDeallocate( _ref );
58 @implementation NSArray( CaseInsensitiveStringArrayCompare )
60 - (BOOL)caseInsensitiveStringMatch:(NSArray *)inArray
64 if( self.count != [inArray count] ) match = NO; // Nil zero len ok
68 for( NSString * next in self )
70 NSString * inNext = inArray[i++];
71 if( ![inNext isKindOfClass: [NSString class]] || ![next isKindOfClass: [NSString class]] )
76 else if( [next caseInsensitiveCompare: inNext] != NSOrderedSame )
89 @protocol CNServiceTypeLocalizerDelegate <NSObject>
90 @property (strong) NSDictionary * localizedServiceTypesDictionary;
93 @interface CNServiceTypeLocalizer : NSValueTransformer
95 id<CNServiceTypeLocalizerDelegate> _delegate;
97 - (instancetype)initWithDelegate:(id<CNServiceTypeLocalizerDelegate>)delegate;
101 @implementation CNServiceTypeLocalizer
103 - (instancetype)initWithDelegate:(id<CNServiceTypeLocalizerDelegate>)delegate
105 if( self = [super init] )
107 _delegate = delegate;
112 + (Class)transformedValueClass
114 return [NSString class];
117 + (BOOL)allowsReverseTransformation
122 - (nullable id)transformedValue:(nullable id)value
126 if( value && _delegate && [_delegate respondsToSelector: @selector(localizedServiceTypesDictionary)] )
128 NSString * localizedValue = [_delegate.localizedServiceTypesDictionary objectForKey: value];
129 if( localizedValue ) result = localizedValue;
137 @implementation NSBrowser( PathArray )
139 - (NSArray *)pathArrayToColumn:(NSInteger)column includeSelectedRow:(BOOL)includeSelection
141 NSMutableArray * pathArray = [NSMutableArray array];
142 if( !includeSelection ) column--;
143 for( NSInteger c = 0 ; c <= column ; c++ )
145 NSBrowserCell *cell = [self selectedCellInColumn: c];
146 if( cell ) [pathArray addObject: [cell stringValue]];
154 static void resolveReply( DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *fullname, const char *hosttarget, uint16_t port, uint16_t txtLen, const unsigned char *txtRecord, void *context );
155 static void browseReply( DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *serviceName, const char *regtype, const char *replyDomain, void *context );
157 @interface CNServiceBrowserView ()
159 @property (strong) NSTableView * instanceTable;
160 @property (strong) NSArrayController * instanceC;
161 @property (strong) NSTableColumn * instanceNameColumn;
162 @property (strong) NSTableColumn * instanceServiceTypeColumn;
163 @property (strong) NSTableColumn * instancePathPopupColumn;
165 @property (strong) CNServiceTypeLocalizer * serviceTypeLocalizer;
167 @property (strong) NSArray * currentDomainPath;
168 @property (strong) NSMutableArray * instanceRs;
169 @property (strong) NSMutableDictionary * instanceD;
170 @property (strong) NSMutableArray * instanceA;
172 @property (strong) dispatch_queue_t instanceBrowseQ;
176 @implementation CNServiceBrowserView
178 @synthesize serviceTypes = _serviceTypes;
180 - (instancetype)initWithFrame:(NSRect)frameRect
182 if( self = [super initWithFrame: frameRect] )
189 - (nullable instancetype)initWithCoder:(NSCoder *)coder
191 if( self = [super initWithCoder: coder] )
199 - (void)contentViewsInit
201 NSRect frame = self.frame;
202 self.instanceC = [[NSArrayController alloc] init];
203 self.serviceTypeLocalizer = [[CNServiceTypeLocalizer alloc] initWithDelegate: (id<CNServiceTypeLocalizerDelegate>)self];
206 NSTableView * tableView = [[NSTableView alloc] initWithFrame: frame];
207 tableView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle;
208 tableView.allowsColumnReordering = NO;
209 tableView.delegate = (id<NSTableViewDelegate>)self;
210 tableView.doubleAction = @selector( doubleAction:);
211 [tableView bind: NSContentBinding toObject: self.instanceC withKeyPath: @"arrangedObjects" options: nil];
212 self.instanceTable = tableView;
214 // Scroll view for table
215 NSScrollView * tableContainer = [[NSScrollView alloc] initWithFrame: frame];
216 tableContainer.autoresizingMask = (NSViewHeightSizable | NSViewWidthSizable);
217 [tableContainer setDocumentView: tableView];
220 NSTableColumn * column = [[NSTableColumn alloc] init];
221 column.resizingMask = (NSTableColumnAutoresizingMask);
222 column.width = frame.size.width / 3;
223 column.minWidth = column.width / 2;
224 NSTextFieldCell * cell = [[NSTextFieldCell alloc] init];
225 cell.truncatesLastVisibleLine = YES;
226 column.dataCell = cell;
227 [column.headerCell setStringValue: NSLocalizedString( @"_dnsBrowser.instances.name", nil )];
228 [column bind: NSValueBinding toObject: self.instanceC withKeyPath: @"arrangedObjects.name" options: nil];
229 [tableView addTableColumn: column];
230 self.instanceNameColumn = column;
232 // Service type column
233 column = [[NSTableColumn alloc] init];
234 column.resizingMask = (NSTableColumnNoResizing);
235 column.width = frame.size.width / 3;
236 column.dataCell = [[NSTextFieldCell alloc] init];
237 [column.headerCell setStringValue: NSLocalizedString( @"_dnsBrowser.instances.type", nil )];
238 [column bind: NSValueBinding toObject: self.instanceC withKeyPath: @"arrangedObjects.serviceType" options: @{ NSValueTransformerBindingOption: self.serviceTypeLocalizer }];
239 [tableView addTableColumn: column];
240 self.instanceServiceTypeColumn = column;
243 column = [[NSTableColumn alloc] init];
244 column.resizingMask = (NSTableColumnNoResizing);
245 column.width = frame.size.width / 3;
246 NSPopUpButtonCell * popUpCell = [[NSPopUpButtonCell alloc] init];
247 popUpCell.pullsDown = YES;
248 popUpCell.arrowPosition = NSPopUpArrowAtBottom;
249 popUpCell.autoenablesItems = YES;
250 popUpCell.preferredEdge = NSRectEdgeMaxY;
251 popUpCell.bezelStyle = NSBezelStyleTexturedSquare;
252 popUpCell.font = [NSFont systemFontOfSize: [NSFont smallSystemFontSize]];
253 column.dataCell = popUpCell;
254 [column.headerCell setStringValue: NSLocalizedString( @"_dnsBrowser.instances.domain", nil )];
255 [column bind: NSContentBinding toObject: self.instanceC withKeyPath: @"arrangedObjects.domainPath" options: nil];
256 [tableView addTableColumn: column];
257 self.instancePathPopupColumn = column;
259 [self addSubview: tableContainer];
264 self.serviceTypes = @[@"_http._tcp"];
265 self.instanceRs = [NSMutableArray array];
266 self.instanceD = [NSMutableDictionary dictionary];
267 self.instanceA = [NSMutableArray array];
269 [self contentViewsInit];
272 - (void) setServiceTypes:(NSArray *)serviceTypes
274 if( ![_serviceTypes isEqualTo: serviceTypes] )
276 _serviceTypes = serviceTypes;
280 - (NSArray *) serviceTypes
282 return( _serviceTypes );
285 - (BOOL)foundInstancesWithMoreThanOneServiceType
289 #if SHOW_SERVICETYPE_IF_SEARCH_COUNT
290 result = (_serviceTypes.count > 1);
292 if( _instanceD.count )
294 NSString * serviceType;
295 for( NSDictionary *next in [_instanceD allValues] )
299 serviceType = next[_CNInstanceKey_serviceType];
302 else if( [next[_CNInstanceKey_serviceType] caseInsensitiveCompare: serviceType] != NSOrderedSame )
314 - (BOOL)foundInstancesInMoreThanCurrentDomainPath
318 if( _instanceD.count )
320 NSArray * selectedPathArray = [[_currentDomainPath reverseObjectEnumerator] allObjects];
321 if( !selectedPathArray.count ) selectedPathArray = [NSArray arrayWithObject: @"local"];
322 for( NSDictionary *next in [_instanceD allValues] )
324 if( [next[_CNInstanceKey_domainPath] caseInsensitiveStringMatch: selectedPathArray] ) continue;
333 #if DEBUG_DOMAIN_POPUPS
340 #pragma mark - Notifications
342 - (void)tableViewSelectionDidChange:(NSNotification *)notification
344 if( _delegate && [_delegate respondsToSelector: @selector(bonjourServiceSelected:type:atDomain:)] &&
345 notification.object == self.instanceTable )
347 NSTableView * table = (NSTableView *)notification.object;
348 NSDictionary * record = nil;
349 if( table.selectedRow >= 0 && table.selectedRow < (NSInteger)[self.instanceC.content count] ) record = (NSDictionary *)self.instanceC.content[table.selectedRow];
351 [_delegate bonjourServiceSelected: record[_CNInstanceKey_name]
352 type: record[_CNInstanceKey_serviceType]
353 atDomain: record ? DomainPathToDNSDomain( [[record[_CNInstanceKey_domainPath] reverseObjectEnumerator] allObjects] ) : nil];
358 #pragma mark - Delegates
360 - (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row
362 (void)tableColumn; // Unused
364 if( tableView == self.instanceTable )
366 if( [cell isKindOfClass: [NSPopUpButtonCell class]] )
368 NSPopUpButtonCell * popCell = cell;
369 if( popCell.numberOfItems > 1 ) popCell.arrowPosition = NSPopUpArrowAtBottom;
370 else popCell.arrowPosition = NSPopUpNoArrow;
376 - (void)tableView:(NSTableView *)tableView didClickTableColumn:(NSTableColumn *)tableColumn
381 - (void) handleBrowseResults
383 dispatch_async( dispatch_get_main_queue(), ^{
384 [self bonjourBrowserServiceBrowseUpdate: self->_instanceA];
388 - (void)bonjourBrowserServiceBrowseUpdate:(NSArray *)services
390 self.instanceC.content = [services sortedArrayUsingComparator: ^( id obj1, id obj2 ) {
391 return (NSComparisonResult)[ obj1[_CNInstanceKey_name] compare: obj2[_CNInstanceKey_name]];
393 [self adjustInstancesColumnWidths];
396 - (void) adjustInstancesColumnWidths
398 self.instanceServiceTypeColumn.hidden = ![self foundInstancesWithMoreThanOneServiceType];
399 self.instancePathPopupColumn.hidden = ![self foundInstancesInMoreThanCurrentDomainPath];
401 if( !self.instanceServiceTypeColumn.hidden || !self.instancePathPopupColumn.hidden )
403 BOOL sizeChanged = NO;
404 CGFloat maxWidthType = 0;
405 CGFloat maxWidthDomain = 0;
406 BOOL needRoomForPopup = NO;
407 NSDictionary * fontAttrType = @{ NSFontAttributeName: ((NSTextFieldCell *)self.instanceServiceTypeColumn.dataCell).font };
408 NSDictionary * fontAttrDomain = @{ NSFontAttributeName: ((NSTextFieldCell *)self.instancePathPopupColumn.dataCell).font };
410 for( NSDictionary * next in self.instanceC.content )
412 NSString * serviceType = [self.serviceTypeLocalizer transformedValue: next[_CNInstanceKey_serviceType]];
413 NSSize nextSize = [serviceType sizeWithAttributes: fontAttrType];
414 maxWidthType = MAX( nextSize.width, maxWidthType );
416 NSArray * path = next[_CNInstanceKey_domainPath];
417 nextSize = [path[0] sizeWithAttributes: fontAttrDomain];
418 maxWidthDomain = MAX( nextSize.width, maxWidthDomain );
419 if( path.count > 1 ) needRoomForPopup = YES;
423 #define POPUP_ARROW 22
425 if( !self.instanceServiceTypeColumn.hidden )
427 maxWidthType += (EDGE_GAP * 2);
428 if( self.instanceServiceTypeColumn.width != maxWidthType )
430 self.instanceServiceTypeColumn.width = self.instanceServiceTypeColumn.minWidth = self.instanceServiceTypeColumn.maxWidth = maxWidthType;
435 if( !self.instancePathPopupColumn.hidden )
437 maxWidthDomain += (EDGE_GAP * 2) + needRoomForPopup ? POPUP_ARROW : 0;
438 if( self.instancePathPopupColumn.width != maxWidthDomain )
440 self.instancePathPopupColumn.width = self.instancePathPopupColumn.minWidth = self.instancePathPopupColumn.maxWidth = maxWidthDomain;
447 [self.instancePathPopupColumn.tableView sizeToFit];
452 #pragma mark - Dispatch
454 static void finalizer( void * context )
456 CNServiceBrowserView *self = (__bridge CNServiceBrowserView *)context;
457 // NSLog( @"finalizer: %@", self );
458 (void)CFBridgingRelease( (__bridge void *)self );
461 #pragma mark - Commands
463 - (void)doubleAction:(id)sender
465 if( _delegate && [_delegate respondsToSelector: @selector(doubleAction:)] &&
466 sender == self.instanceTable )
468 NSTableView * table = (NSTableView *)sender;
469 NSDictionary * record = nil;
470 if( table.selectedRow >= 0 && table.selectedRow < (NSInteger)[self.instanceC.content count] ) record = (NSDictionary *)self.instanceC.content[table.selectedRow];
471 [_delegate doubleAction: record[_CNInstanceKey_resolveUrl]];
475 - (void)newServiceBrowse:(NSArray *)domainPath
477 if( _serviceTypes.count)
479 self.instanceC.content = nil;
480 [self browseForServiceTypes: _serviceTypes inDomainPath: domainPath];
484 - (void)browseForServiceTypes:(NSArray *)serviceTypes inDomainPath:(NSArray *)domainPath
486 if( serviceTypes.count /*&& domainPath.count*/ )
488 _serviceTypes = [serviceTypes copy];
489 _currentDomainPath = [domainPath copy];
491 NSString * domainStr = DomainPathToDNSDomain( _currentDomainPath );
493 [_instanceRs removeAllObjects];
494 if( !_instanceBrowseQ )
496 self.instanceBrowseQ = dispatch_queue_create( "DNSServiceBrowse", DISPATCH_QUEUE_PRIORITY_DEFAULT );
497 dispatch_set_context( _instanceBrowseQ, (void *)CFBridgingRetain( self ) );
498 dispatch_set_finalizer_f( _instanceBrowseQ, finalizer );
501 dispatch_sync( _instanceBrowseQ, ^{
502 [self->_instanceD removeAllObjects];
503 [self->_instanceA removeAllObjects];
506 DNSServiceErrorType error;
507 DNSServiceRef mainRef;
508 if( (error = DNSServiceCreateConnection( &mainRef )) != 0 )
509 NSLog(@"DNSServiceCreateConnection failed error: %ld", error);
512 for( NSString * nextService in _serviceTypes )
514 DNSServiceRef ref = mainRef;
515 if( (error = DNSServiceBrowse( &ref, kDNSServiceFlagsShareConnection, 0, [nextService UTF8String], [domainStr UTF8String], browseReply, (__bridge void *)self )) != 0 )
516 NSLog(@"DNSServiceBrowse failed error: %ld", error);
519 [_instanceRs addObject: [[_DNSServiceRefWrapper alloc] initWithRef: ref]];
522 [_instanceRs addObject: [[_DNSServiceRefWrapper alloc] initWithRef: mainRef]];
525 error = DNSServiceSetDispatchQueue( mainRef, _instanceBrowseQ );
526 if( error ) NSLog( @"DNSServiceSetDispatchQueue error: %d", error );
532 - (void)resolveServiceInstance:(NSMutableDictionary *)record
534 __weak NSDictionary * weakRecord = record;
536 DNSServiceErrorType error;
537 NSString * domainPath = DomainPathToDNSDomain( record[_CNInstanceKey_domainPath] );
539 if( (error = DNSServiceResolve( &ref, (DNSServiceFlags)0, kDNSServiceInterfaceIndexAny, [record[_CNInstanceKey_name] UTF8String], [record[_CNInstanceKey_serviceType] UTF8String], [domainPath UTF8String], resolveReply, (__bridge void *)weakRecord )) != 0 )
541 NSLog(@"DNSServiceResolve failed error: %ld", error);
545 record[_CNInstanceKey_resolveInstance] = [[_DNSServiceRefWrapper alloc] initWithRef: ref];
546 error = DNSServiceSetDispatchQueue( ref, _instanceBrowseQ );
547 if( error ) NSLog( @"resolve DNSServiceSetDispatchQueue error: %d", error );
551 #pragma mark - Static Callbacks
553 static void resolveReply( DNSServiceRef sdRef,
554 DNSServiceFlags flags,
555 uint32_t interfaceIndex,
556 DNSServiceErrorType errorCode,
557 const char *fullname,
558 const char *hosttarget,
559 uint16_t port, /* In network byte order */
561 const unsigned char *txtRecord,
564 (void)sdRef; // Unused
565 (void)flags; // Unused
566 (void)interfaceIndex; // Unused
567 (void)errorCode; // Unused
568 (void)fullname; // Unused
569 __weak NSMutableDictionary * record = (__bridge __weak NSMutableDictionary *)context;
570 if( record && hosttarget )
572 NSURLComponents * urlComponents = [[NSURLComponents alloc] init];
573 urlComponents.scheme = @"http";
574 urlComponents.host = [NSString stringWithUTF8String: hosttarget];
575 if( TXTRecordContainsKey( txtLen, txtRecord, "path" ) )
578 const u_char * valuePtr = TXTRecordGetValuePtr( txtLen, txtRecord, "path", &valueLen );
579 urlComponents.path = (__bridge_transfer NSString *)CFStringCreateWithBytes( kCFAllocatorDefault, valuePtr, valueLen, kCFStringEncodingUTF8, false );
581 if( port ) urlComponents.port = [NSNumber numberWithUnsignedShort: ntohs( port )];
582 record[_CNInstanceKey_resolveUrl] = urlComponents.URL;
586 static void browseReply( DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *serviceName, const char *regtype, const char *replyDomain, void *context )
588 (void)sdRef; // Unused
589 (void)interfaceIndex; // Unused
590 (void)errorCode; // Unused
591 CNServiceBrowserView *self = (__bridge CNServiceBrowserView *)context;
592 char fullNameBuffer[kDNSServiceMaxDomainName];
593 if( DNSServiceConstructFullName( fullNameBuffer, serviceName, regtype, replyDomain ) == kDNSServiceErr_NoError )
595 NSString *fullName = @(fullNameBuffer);
596 NSString *name = [NSString stringWithUTF8String: serviceName];
597 NSArray *pathArray = DNSDomainToDomainPath( [NSString stringWithUTF8String: replyDomain] );
599 if( flags & kDNSServiceFlagsAdd )
602 NSString * newServiceType = [[NSString stringWithUTF8String: regtype] stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString: @"."]];
603 NSString * oldServiceType = [self.instanceD objectForKey: name][_CNInstanceKey_serviceType];
604 if( oldServiceType && ![newServiceType isEqualToString: oldServiceType] )
606 NSInteger newIndex = [self.serviceTypes indexOfObject: newServiceType];
607 NSInteger oldIndex = [self.serviceTypes indexOfObject: oldServiceType];
608 if( newIndex != NSNotFound && oldIndex != NSNotFound && oldIndex < newIndex ) okToAdd = NO;
612 NSMutableDictionary * record = [NSMutableDictionary dictionary];
613 record[_CNInstanceKey_fullName] = fullName;
614 record[_CNInstanceKey_name] = name;
615 record[_CNInstanceKey_serviceType] = newServiceType;
616 record[_CNInstanceKey_domainPath] = [[pathArray reverseObjectEnumerator] allObjects];
617 [self.instanceD setObject: record
619 [self resolveServiceInstance: record];
624 NSString * newServiceType = [[NSString stringWithUTF8String: regtype] stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString: @"."]];
625 NSDictionary * oldRecord = [self.instanceD objectForKey: name];
626 if( [oldRecord[_CNInstanceKey_serviceType] isEqualToString: newServiceType] )
628 [self.instanceD removeObjectForKey: name];
632 if( !(flags & kDNSServiceFlagsMoreComing) )
634 dispatch_async( dispatch_get_main_queue(), ^{
635 [self.instanceA setArray: [[self.instanceD allValues] sortedArrayUsingComparator: ^( id obj1, id obj2 ) {
636 return (NSComparisonResult)[obj1[_CNInstanceKey_name] compare: obj2[_CNInstanceKey_name] options: NSCaseInsensitiveSearch];
638 [self handleBrowseResults];